controllers.Wiki)page.updated)view.scala.html)page.updated)이 문서는 현재 AhaWiki에 구현된 WebSocket 기능(페이지 뷰 화면의 실시간 커서 공유 + 페이지 갱신 알림)을 코드 기준으로 정리합니다.
GET /ws/w/*nameEncoded → controllers.Wiki.watch(nameEncoded: String)./w/...)를 /ws/w/...로 치환해 WebSocket URL을 만들고,https면 wss://, 그 외에는 ws://를 사용합니다.즉, 사용자가 문서 보기 화면에 진입하면 브라우저 JS가 해당 문서용 WebSocket에 자동 연결을 시도합니다.
controllers.Wiki)wiki:<siteId>:<pageId> 형식입니다.siteId + pageId 조합으로 같은 이름의 문서를 분리합니다.PageCursorHubPageCursorHub는 서버 메모리 내 구독자 맵을 가지고 브로드캐스트를 수행합니다.
subscribers: TrieMap[roomKey, TrieMap[connectionId, PageSubscriber]]PageSubscriber는 queue(outbound queue)와 saveSenderId를 가짐.subscribe: room에 연결 추가unsubscribe: 연결 제거 및 queue completebroadcast: 발신자(connectionId) 제외하고 메시지 전송setSaveSenderId: 연결별 저장용 식별자 저장broadcastPageUpdated: 동일 saveSenderId 연결은 제외하고 page.updated 전송참고: 이 허브는 프로세스 메모리 기반이라, 다중 인스턴스(서버 여러 대) 환경에서는 인스턴스 간 동기화가 없습니다.
watch)watch(nameEncoded)에서 수행하는 핵심 로직:
nameEncoded를 decode해 페이지명 복원Site) 조회WikiPermission.isReadable) 검사Forbidden("Permission denied.") 반환이로 인해 읽기 권한이 없는 페이지는 소켓 연결 자체가 거부됩니다.
서버 sink는 inbound JSON의 type에 따라 아래를 처리합니다.
cursor.move{ type: "cursor.move", x, y }x, y는 숫자로 읽고 각각 [0.0, 1.0] 범위로 clamptype, siteId, pageId, senderId, x, y, tscursor.hello{ type: "cursor.hello", saveSenderId? }saveSenderId를 연결 상태에 저장senderId, 사용자 nickname, profileImageUrl을 담은 hello를 타 사용자에게 브로드캐스트mapMaterializedValue)cursor.hello 1회 pushcursor.hello 브로드캐스트watchTermination)page.updated)문서 저장 API(save)에서 실제 저장 성공 후 다음 payload를 만들어 브로드캐스트합니다.
type: "page.updated"pageNamerevision (새 리비전)editorNicknamedateInserted전송 방식은 broadcastPageUpdated(roomKey, saveSenderId, payload)이며,
saveSenderId와 동일한 saveSenderId를 가진 구독자에게는 알림을 제외합니다.view.scala.html)문서 보기 페이지 JS가 WebSocket을 담당합니다.
connect()에서 new WebSocket(wsUrl) 생성onopen 시 saveSenderId를 생성 후 cursor.hello 전송 (브라우저 쿠키 ID + 탭별 sessionStorage ID 조합)saveSenderId는 브라우저 공통 ID(쿠키, 1년) + 탭 고유 ID(sessionStorage)를 결합해, 같은 브라우저의 다른 탭은 서로 다른 값이 되도록 합니다.onclose 시 reconnectDelayMs 후 재연결 시도onerror/onclose는 console warn 로그 출력cursor.hellosenderId를 기준으로 상대 커서 메타(nickname, profileImageUrl) 저장senderId 확정 로직 수행cursor.hello를 다시 보냄cursor.moveupsertCursor(senderId, x, y) 호출requestAnimationFrame)가 보간(smoothing) 이동 처리page.updatedrevision이 유효하고, 마지막으로 본 revision과 다를 때만 처리${editorNickname} updated this page. Would you like to refresh? 토스트 노출cursor.move 전송e.pageX / window.width, e.pageY / window.height로 정규화하여 [0,1] 범위로 보냄클라이언트 → 서버:
cursor.hello{ "type": "cursor.hello", "saveSenderId": "..." }
cursor.move{ "type": "cursor.move", "x": 0.42, "y": 0.77 }
서버 → 클라이언트:
cursor.hello{ "type": "cursor.hello", "senderId": "...", "nickname": "...", "profileImageUrl": "..." }
cursor.move{ "type": "cursor.move", "siteId": 1, "pageId": "Home", "senderId": "...", "x": 0.42, "y": 0.77, "ts": 1710000000000 }
page.updated{ "type": "page.updated", "pageName": "Home", "revision": 12, "editorNickname": "Alice", "dateInserted": "2026-05-11T10:20:30" }
Source.queue[String](32, OverflowStrategy.dropHead)이므로 소비 지연 시 오래된 outbound 메시지가 버려질 수 있습니다.saveSenderId를 함께 전송합니다. 서버에서 409 Conflict를 반환하면 최신 revision을 재조회 후 최대 3회 자동 재시도하고, 모두 실패 시 non-blocking 토스트를 표시합니다 (페이지 강제 새로고침 없음).page.updated 이벤트를 칸반 실시간 동기화에 활용합니다 — 자세한 내용은 Kanban Realtime] 참조.page.updated)같은 페이지를 여러 사용자가 동시에 보고 있을 때, **다른 사용자의 저장**을 감지해 새로고침을 유도하는 기능입니다.
페이지 저장 성공 시점에 해당 페이지 room으로 page.updated 이벤트를 broadcast 합니다.
type: "page.updated"pageName: 페이지 이름revision: 최신 revision 번호editorNickname: 저장 사용자 닉네임(가능 시)dateInserted: 서버 timestamp(ISO8601)Wiki/view 페이지의 WebSocket onmessage에서 page.updated 이벤트를 처리합니다.
${editorNickname} updated this page. Would you like to refresh?Refresh → 클릭 시 window.location.reload() 실행wiki:page.updated DOM CustomEvent 발행page.updated 수신 후 document.dispatchEvent(new CustomEvent('wiki:page.updated', { detail: { pageName, revision, editorNickname } })) 를 발행합니다.
이를 통해 페이지에 임베드된 컴포넌트(예: Kanban)가 자신의 업데이트 로직을 독립적으로 구독할 수 있습니다.
AhaWiki.Kanban.js가 wiki:page.updated 이벤트를 수신하면 보드를 자동으로 갱신합니다.
GET /w/{page}?action=raw 으로 최신 원문 fetchparseKanbanTextrerenderColumns자세한 내용: Kanban Realtime]
revision에 대해서는 토스트를 1회만 표시page.updated 이벤트를 페이지 room에 전송합니다.Refresh를 누르면 페이지가 새로고침되어 최신 revision을 확인할 수 있습니다.page.updated 이벤트를 전송합니다.view.scala.html이 wiki:page.updated DOM CustomEvent를 발행합니다.AhaWiki.Kanban.js가 이벤트를 수신 → 원문 fetch → 병합 → 보드 자동 갱신.Similar pages by cosine similarity. Words after page name are term frequency.